/* ========================================================================
 * Copyright (c) 2005-2021 The OPC Foundation, Inc. All rights reserved.
 *
 * OPC Foundation MIT License 1.00
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * The complete license agreement can be found here:
 * http://opcfoundation.org/License/MIT/1.00/
 * ======================================================================*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace Opc.Ua.PubSub.Configuration
{
    /// <summary>
    /// <para>Entity responsible to configure a PubSub Application</para>
    /// <para>
    /// It has methods for adding/removing configuration objects to a root <see cref="PubSubConfigurationDataType"/> object.
    /// When the root <see cref="PubSubConfigurationDataType"/> object is modified there are various events raised to allow reaction to configuration changes.
    /// Each child object from parent <see cref="PubSubConfigurationDataType"/> object has a configurationId associated to it and it can be used to alter configuration.
    /// The configurationId can be obtained using the <see cref="FindIdForObject(object)"/> method.
    /// </para>
    /// </summary>
    public class UaPubSubConfigurator
    {
        /// <summary>
        /// Value of an uninitialized identifier.
        /// </summary>
        internal static uint InvalidId;

        private readonly Lock m_lock = new();
        private readonly Dictionary<uint, object> m_idsToObjects;
        private readonly Dictionary<object, uint> m_objectsToIds;
        private readonly Dictionary<uint, PubSubState> m_idsToPubSubState;
        private readonly Dictionary<uint, uint> m_idsToParentId;
        private uint m_nextId = 1;

        /// <summary>
        /// Event that is triggered when a published data set is added to the configurator
        /// </summary>
        public event EventHandler<PublishedDataSetEventArgs> PublishedDataSetAdded;

        /// <summary>
        /// Event that is triggered when a published data set is removed from the configurator
        /// </summary>
        public event EventHandler<PublishedDataSetEventArgs> PublishedDataSetRemoved;

        /// <summary>
        /// Event that is triggered when an extension field is added to a published data set
        /// </summary>
        public event EventHandler<ExtensionFieldEventArgs> ExtensionFieldAdded;

        /// <summary>
        /// Event that is triggered when an extension field is removed from a published data set
        /// </summary>
        public event EventHandler<ExtensionFieldEventArgs> ExtensionFieldRemoved;

        /// <summary>
        /// Event that is triggered when a connection is added to the configurator
        /// </summary>
        public event EventHandler<ConnectionEventArgs> ConnectionAdded;

        /// <summary>
        /// Event that is triggered when a connection is removed from the configurator
        /// </summary>
        public event EventHandler<ConnectionEventArgs> ConnectionRemoved;

        /// <summary>
        /// Event that is triggered when a WriterGroup is added to a connection
        /// </summary>
        public event EventHandler<WriterGroupEventArgs> WriterGroupAdded;

        /// <summary>
        /// Event that is triggered when a WriterGroup is removed from a connection
        /// </summary>
        public event EventHandler<WriterGroupEventArgs> WriterGroupRemoved;

        /// <summary>
        /// Event that is triggered when a ReaderGroup is added to a connection
        /// </summary>
        public event EventHandler<ReaderGroupEventArgs> ReaderGroupAdded;

        /// <summary>
        /// Event that is triggered when a ReaderGroup is removed from a connection
        /// </summary>
        public event EventHandler<ReaderGroupEventArgs> ReaderGroupRemoved;

        /// <summary>
        /// Event that is triggered when a DataSetWriter is added to a WriterGroup
        /// </summary>
        public event EventHandler<DataSetWriterEventArgs> DataSetWriterAdded;

        /// <summary>
        /// Event that is triggered when a DataSetWriter is removed from a WriterGroup
        /// </summary>
        public event EventHandler<DataSetWriterEventArgs> DataSetWriterRemoved;

        /// <summary>
        /// Event that is triggered when a DataSetreader is added to a ReaderGroup
        /// </summary>
        public event EventHandler<DataSetReaderEventArgs> DataSetReaderAdded;

        /// <summary>
        /// Event that is triggered when a DataSetreader is removed from a ReaderGroup
        /// </summary>
        public event EventHandler<DataSetReaderEventArgs> DataSetReaderRemoved;

        /// <summary>
        /// Event raised when the state of a configuration object is changed
        /// </summary>
        public event EventHandler<PubSubStateChangedEventArgs> PubSubStateChanged;

        /// <summary>
        /// Create new instance of <see cref="UaPubSubConfigurator"/>.
        /// </summary>
        public UaPubSubConfigurator()
        {
            m_idsToObjects = [];
            m_objectsToIds = [];
            m_idsToPubSubState = [];
            m_idsToParentId = [];

            PubSubConfiguration = new PubSubConfigurationDataType
            {
                Connections = [],
                PublishedDataSets = []
            };

            //remember configuration id
            uint id = m_nextId++;
            m_objectsToIds.Add(PubSubConfiguration, id);
            m_idsToObjects.Add(id, PubSubConfiguration);
            m_idsToPubSubState.Add(id, GetInitialPubSubState(PubSubConfiguration));
        }

        /// <summary>
        /// Get reference to <see cref="PubSubConfigurationDataType"/> instance that maintains the configuration for this <see cref="UaPubSubConfigurator"/>.
        /// </summary>
        public PubSubConfigurationDataType PubSubConfiguration { get; }

        /// <summary>
        /// Search a configured <see cref="PublishedDataSetDataType"/> with the specified name and return it
        /// </summary>
        /// <param name="name">Name of the object to be found.
        /// Returns null if name was not found.</param>
        public PublishedDataSetDataType FindPublishedDataSetByName(string name)
        {
            foreach (PublishedDataSetDataType publishedDataSet in PubSubConfiguration
                .PublishedDataSets)
            {
                if (name == publishedDataSet.Name)
                {
                    return publishedDataSet;
                }
            }
            return null;
        }

        /// <summary>
        /// Search objects in current configuration and return them
        /// </summary>
        /// <param name="id">Id of the object to be found.
        /// Returns null if id was not found.</param>
        public object FindObjectById(uint id)
        {
            if (m_idsToObjects.TryGetValue(id, out object objectById))
            {
                return objectById;
            }
            return null;
        }

        /// <summary>
        /// Search id for specified configuration object.
        /// </summary>
        /// <param name="configurationObject">The object whose id is searched.</param>
        /// <returns>Returns <see cref="InvalidId"/> if object was not found.</returns>
        public uint FindIdForObject(object configurationObject)
        {
            if (m_objectsToIds.TryGetValue(configurationObject, out uint id))
            {
                return id;
            }
            return InvalidId;
        }

        /// <summary>
        /// Search <see cref="PubSubState"/> for specified configuration object.
        /// </summary>
        /// <param name="configurationObject">The object whose <see cref="PubSubState"/> is searched.</param>
        /// <returns>Returns <see cref="PubSubState"/> if the object.</returns>
        public PubSubState FindStateForObject(object configurationObject)
        {
            uint id = FindIdForObject(configurationObject);
            if (m_idsToPubSubState.TryGetValue(id, out PubSubState pubSubState))
            {
                return pubSubState;
            }
            return PubSubState.Error;
        }

        /// <summary>
        /// Search <see cref="PubSubState"/> for specified configuration object.
        /// </summary>
        /// <param name="id">The id  of the object which <see cref="PubSubState"/> is searched.</param>
        /// <returns>Returns <see cref="PubSubState"/> if the object.</returns>
        public PubSubState FindStateForId(uint id)
        {
            if (m_idsToPubSubState.TryGetValue(id, out PubSubState pubsubState))
            {
                return pubsubState;
            }
            return PubSubState.Error;
        }

        /// <summary>
        /// Find the parent configuration object for a configuration object
        /// </summary>
        public object FindParentForObject(object configurationObject)
        {
            uint id = FindIdForObject(configurationObject);
            if (id != InvalidId && m_idsToParentId.TryGetValue(id, out uint parentId))
            {
                return FindObjectById(parentId);
            }
            return null;
        }

        /// <summary>
        /// Find children ids for specified object
        /// </summary>
        public List<uint> FindChildrenIdsForObject(object configurationObject)
        {
            uint parentId = FindIdForObject(configurationObject);

            var childrenIds = new List<uint>();
            if (parentId != InvalidId && m_idsToParentId.ContainsValue(parentId))
            {
                foreach (uint key in m_idsToParentId.Keys)
                {
                    if (m_idsToParentId[key] == parentId)
                    {
                        childrenIds.Add(key);
                    }
                }
            }
            return childrenIds;
        }

        /// <summary>
        /// Load the specified configuration
        /// </summary>
        /// <param name="configFilePath">From where to load configuration</param>
        /// <param name="replaceExisting"> flag that indicates if current configuration is overwritten</param>
        /// <exception cref="ArgumentNullException"><paramref name="configFilePath"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException"></exception>
        public void LoadConfiguration(string configFilePath, bool replaceExisting = true)
        {
            // validate input argument
            if (configFilePath == null)
            {
                throw new ArgumentNullException(nameof(configFilePath));
            }
            if (!File.Exists(configFilePath))
            {
                throw new ArgumentException(
                    "The specified file {0} does not exist",
                    configFilePath);
            }
            PubSubConfigurationDataType pubSubConfiguration = UaPubSubConfigurationHelper
                .LoadConfiguration(
                    configFilePath);

            LoadConfiguration(pubSubConfiguration, replaceExisting);
        }

        /// <summary>
        /// Load the specified configuration
        /// </summary>
        /// <param name="pubSubConfiguration">The configuration</param>
        /// <param name="replaceExisting"> flag that indicates if current configuration is overwritten</param>
        public void LoadConfiguration(
            PubSubConfigurationDataType pubSubConfiguration,
            bool replaceExisting = true)
        {
            lock (m_lock)
            {
                if (replaceExisting)
                {
                    //remove previous configured published data sets
                    if (PubSubConfiguration != null &&
                        PubSubConfiguration.PublishedDataSets.Count > 0)
                    {
                        foreach (PublishedDataSetDataType publishedDataSet in pubSubConfiguration
                            .PublishedDataSets)
                        {
                            RemovePublishedDataSet(publishedDataSet);
                        }
                    }

                    //remove previous configured connections
                    if (PubSubConfiguration != null && PubSubConfiguration.Connections.Count > 0)
                    {
                        foreach (PubSubConnectionDataType connection in PubSubConfiguration
                            .Connections
                            .ToArray())
                        {
                            RemoveConnection(connection);
                        }
                    }

                    PubSubConfiguration?.Connections.Clear();
                    PubSubConfiguration?.PublishedDataSets.Clear();
                }

                //first load Published DataSet information
                foreach (PublishedDataSetDataType publishedDataSet in pubSubConfiguration
                    .PublishedDataSets)
                {
                    AddPublishedDataSet(publishedDataSet);
                }

                foreach (PubSubConnectionDataType pubSubConnectionDataType in pubSubConfiguration
                    .Connections)
                {
                    // handle empty names
                    if (string.IsNullOrEmpty(pubSubConnectionDataType.Name))
                    {
                        //set default name
                        pubSubConnectionDataType.Name = "Connection_" + (m_nextId + 1);
                    }
                    AddConnection(pubSubConnectionDataType);
                }
            }
        }

        /// <summary>
        /// Add a published data set to current configuration.
        /// </summary>
        /// <param name="publishedDataSetDataType">The <see cref="PublishedDataSetDataType"/> object to be added to configuration.</param>
        /// <exception cref="ArgumentException"></exception>
        public StatusCode AddPublishedDataSet(PublishedDataSetDataType publishedDataSetDataType)
        {
            if (m_objectsToIds.ContainsKey(publishedDataSetDataType))
            {
                throw new ArgumentException(
                    "This PublishedDataSetDataType instance is already added to the configuration.");
            }
            try
            {
                lock (m_lock)
                {
                    //validate duplicate name
                    bool duplicateName = false;
                    foreach (PublishedDataSetDataType publishedDataSet in PubSubConfiguration
                        .PublishedDataSets)
                    {
                        if (publishedDataSetDataType.Name == publishedDataSet.Name)
                        {
                            duplicateName = true;
                            break;
                        }
                    }
                    if (duplicateName)
                    {
                        Utils.Trace(
                            Utils.TraceMasks.Error,
                            "Attempted to add PublishedDataSetDataType with duplicate name = {0}",
                            publishedDataSetDataType.Name);
                        return StatusCodes.BadBrowseNameDuplicated;
                    }

                    uint newPublishedDataSetId = m_nextId++;
                    //remember connection
                    m_idsToObjects.Add(newPublishedDataSetId, publishedDataSetDataType);
                    m_objectsToIds.Add(publishedDataSetDataType, newPublishedDataSetId);
                    PubSubConfiguration.PublishedDataSets.Add(publishedDataSetDataType);

                    // raise PublishedDataSetAdded event
                    PublishedDataSetAdded?.Invoke(
                        this,
                        new PublishedDataSetEventArgs
                        {
                            PublishedDataSetId = newPublishedDataSetId,
                            PublishedDataSetDataType = publishedDataSetDataType
                        });

                    publishedDataSetDataType.ExtensionFields ??= [];
                    var extensionFields = new KeyValuePairCollection(
                        publishedDataSetDataType.ExtensionFields);
                    publishedDataSetDataType.ExtensionFields.Clear();
                    foreach (KeyValuePair extensionField in extensionFields)
                    {
                        AddExtensionField(newPublishedDataSetId, extensionField);
                    }
                    return StatusCodes.Good;
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.AddPublishedDataSet: Exception");
            }

            //todo implement state validation
            return StatusCodes.Bad;
        }

        /// <summary>
        /// Removes a published data set from current configuration.
        /// </summary>
        /// <param name="publishedDataSetId">Id of the published data set to be removed.</param>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> if operation is successful,
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> otherwise.
        /// </returns>
        public StatusCode RemovePublishedDataSet(uint publishedDataSetId)
        {
            lock (m_lock)
            {
                if (FindObjectById(
                    publishedDataSetId) is not PublishedDataSetDataType publishedDataSetDataType)
                {
                    // Unexpected exception
                    Utils.Trace(
                        Utils.TraceMasks.Information,
                        "Current configuration does not contain PublishedDataSetDataType with ConfigId = {0}",
                        publishedDataSetId);
                    return StatusCodes.Good;
                }
                return RemovePublishedDataSet(publishedDataSetDataType);
            }
        }

        /// <summary>
        /// Removes a published data set from current configuration.
        /// </summary>
        /// <param name="publishedDataSetDataType">The published data set to be removed.</param>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> if operation is successful,
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> otherwise.
        /// </returns>
        public StatusCode RemovePublishedDataSet(PublishedDataSetDataType publishedDataSetDataType)
        {
            try
            {
                lock (m_lock)
                {
                    uint publishedDataSetId = FindIdForObject(publishedDataSetDataType);
                    if (publishedDataSetDataType != null && publishedDataSetId != InvalidId)
                    {
                        /*A successful removal of the PublishedDataSetType Object removes all associated DataSetWriter Objects.
                         * Before the Objects are removed, their state is changed to Disabled_0*/

                        // Find all associated DataSetWriter objects
                        foreach (PubSubConnectionDataType connection in PubSubConfiguration
                            .Connections)
                        {
                            foreach (WriterGroupDataType writerGroup in connection.WriterGroups)
                            {
                                foreach (DataSetWriterDataType dataSetWriter in writerGroup
                                    .DataSetWriters
                                    .ToArray())
                                {
                                    if (dataSetWriter.DataSetName == publishedDataSetDataType.Name)
                                    {
                                        RemoveDataSetWriter(dataSetWriter);
                                    }
                                }
                            }
                        }

                        PubSubConfiguration.PublishedDataSets.Remove(publishedDataSetDataType);

                        //remove all references from dictionaries
                        m_idsToObjects.Remove(publishedDataSetId);
                        m_objectsToIds.Remove(publishedDataSetDataType);
                        m_idsToParentId.Remove(publishedDataSetId);
                        m_idsToPubSubState.Remove(publishedDataSetId);

                        PublishedDataSetRemoved?.Invoke(
                            this,
                            new PublishedDataSetEventArgs
                            {
                                PublishedDataSetId = publishedDataSetId,
                                PublishedDataSetDataType = publishedDataSetDataType
                            });
                        return StatusCodes.Good;
                    }
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.RemovePublishedDataSet: Exception");
            }

            return StatusCodes.BadNodeIdUnknown;
        }

        /// <summary>
        /// Add Extension field to the specified publishedDataSet
        /// </summary>
        public StatusCode AddExtensionField(
            uint publishedDataSetConfigId,
            KeyValuePair extensionField)
        {
            lock (m_lock)
            {
                if (FindObjectById(
                    publishedDataSetConfigId) is not PublishedDataSetDataType publishedDataSetDataType)
                {
                    return StatusCodes.BadNodeIdInvalid;
                }
                if (publishedDataSetDataType.ExtensionFields == null)
                {
                    publishedDataSetDataType.ExtensionFields = [];
                }
                else
                {
                    //validate duplicate name
                    bool duplicateName = false;
                    foreach (KeyValuePair element in publishedDataSetDataType.ExtensionFields)
                    {
                        if (element.Key == extensionField.Key)
                        {
                            duplicateName = true;
                            break;
                        }
                    }
                    if (duplicateName)
                    {
                        Utils.Trace(
                            Utils.TraceMasks.Error,
                            "AddExtensionField -  A field with the name already exists. Duplicate name = {0}",
                            extensionField.Key);
                        return StatusCodes.BadNodeIdExists;
                    }
                }
                uint newextensionFieldId = m_nextId++;
                //remember connection
                m_idsToObjects.Add(newextensionFieldId, extensionField);
                m_objectsToIds.Add(extensionField, newextensionFieldId);
                publishedDataSetDataType.ExtensionFields.Add(extensionField);

                // raise ExtensionFieldAdded event
                ExtensionFieldAdded?.Invoke(
                    this,
                    new ExtensionFieldEventArgs
                    {
                        PublishedDataSetId = publishedDataSetConfigId,
                        ExtensionFieldId = newextensionFieldId,
                        ExtensionField = extensionField
                    });

                return StatusCodes.Good;
            }
        }

        /// <summary>
        /// Removes an extension field from a published data set
        /// </summary>
        public StatusCode RemoveExtensionField(
            uint publishedDataSetConfigId,
            uint extensionFieldConfigId)
        {
            lock (m_lock)
            {
                if ((FindObjectById(
                        publishedDataSetConfigId) is not PublishedDataSetDataType publishedDataSetDataType) ||
                    (FindObjectById(
                        extensionFieldConfigId) is not KeyValuePair extensionFieldToRemove))
                {
                    return StatusCodes.BadNodeIdInvalid;
                }
                if (publishedDataSetDataType.ExtensionFields == null)
                {
                    publishedDataSetDataType.ExtensionFields = [];
                    return StatusCodes.BadNodeIdInvalid;
                }
                // locate the extension field
                foreach (KeyValuePair extensionField in publishedDataSetDataType.ExtensionFields
                    .ToArray())
                {
                    if (extensionField.Equals(extensionFieldToRemove))
                    {
                        publishedDataSetDataType.ExtensionFields.Remove(extensionFieldToRemove);

                        // raise ExtensionFieldRemoved event
                        ExtensionFieldRemoved?.Invoke(
                            this,
                            new ExtensionFieldEventArgs
                            {
                                PublishedDataSetId = publishedDataSetConfigId,
                                ExtensionFieldId = extensionFieldConfigId,
                                ExtensionField = extensionField
                            });
                        return StatusCodes.Good;
                    }
                }
            }
            return StatusCodes.BadNodeIdInvalid;
        }

        /// <summary>
        /// Add a connection to current configuration.
        /// </summary>
        /// <param name="pubSubConnectionDataType">The <see cref="PubSubConnectionDataType"/> object that configures the new connection.</param>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The connection was added with success.
        /// - <see cref="StatusCodes.BadBrowseNameDuplicated"/> An Object with the name already exists.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error adding the connection.
        /// </returns>
        /// <exception cref="ArgumentException"></exception>
        public StatusCode AddConnection(PubSubConnectionDataType pubSubConnectionDataType)
        {
            if (m_objectsToIds.ContainsKey(pubSubConnectionDataType))
            {
                throw new ArgumentException(
                    "This PubSubConnectionDataType instance is already added to the configuration.");
            }
            try
            {
                lock (m_lock)
                {
                    //validate connection name
                    bool duplicateName = false;
                    foreach (PubSubConnectionDataType connection in PubSubConfiguration.Connections)
                    {
                        if (connection.Name == pubSubConnectionDataType.Name)
                        {
                            duplicateName = true;
                            break;
                        }
                    }
                    if (duplicateName)
                    {
                        Utils.Trace(
                            Utils.TraceMasks.Error,
                            "Attempted to add PubSubConnectionDataType with duplicate name = {0}",
                            pubSubConnectionDataType.Name);
                        return StatusCodes.BadBrowseNameDuplicated;
                    }

                    // remember collections
                    var writerGroups = new WriterGroupDataTypeCollection(
                        pubSubConnectionDataType.WriterGroups);
                    pubSubConnectionDataType.WriterGroups.Clear();
                    var readerGroups = new ReaderGroupDataTypeCollection(
                        pubSubConnectionDataType.ReaderGroups);
                    pubSubConnectionDataType.ReaderGroups.Clear();

                    uint newConnectionId = m_nextId++;
                    //remember connection
                    m_idsToObjects.Add(newConnectionId, pubSubConnectionDataType);
                    m_objectsToIds.Add(pubSubConnectionDataType, newConnectionId);
                    // remember parent id
                    m_idsToParentId.Add(newConnectionId, FindIdForObject(PubSubConfiguration));
                    //remember initial state
                    m_idsToPubSubState.Add(
                        newConnectionId,
                        GetInitialPubSubState(pubSubConnectionDataType));

                    PubSubConfiguration.Connections.Add(pubSubConnectionDataType);

                    // raise ConnectionAdded event
                    ConnectionAdded?.Invoke(
                        this,
                        new ConnectionEventArgs
                        {
                            ConnectionId = newConnectionId,
                            PubSubConnectionDataType = pubSubConnectionDataType
                        });
                    //handler reader & writer groups
                    foreach (WriterGroupDataType writerGroup in writerGroups)
                    {
                        // handle empty names
                        if (string.IsNullOrEmpty(writerGroup.Name))
                        {
                            //set default name
                            writerGroup.Name = "WriterGroup_" + (m_nextId + 1);
                        }
                        AddWriterGroup(newConnectionId, writerGroup);
                    }
                    foreach (ReaderGroupDataType readerGroup in readerGroups)
                    {
                        // handle empty names
                        if (string.IsNullOrEmpty(readerGroup.Name))
                        {
                            //set default name
                            readerGroup.Name = "ReaderGroup_" + (m_nextId + 1);
                        }
                        AddReaderGroup(newConnectionId, readerGroup);
                    }

                    return StatusCodes.Good;
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.AddConnection: Exception");
            }
            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Removes a connection from current configuration.
        /// </summary>
        /// <param name="connectionId">Id of the connection to be removed.</param>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The Connection was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the Connection.
        /// </returns>
        public StatusCode RemoveConnection(uint connectionId)
        {
            lock (m_lock)
            {
                if (FindObjectById(
                    connectionId) is not PubSubConnectionDataType pubSubConnectionDataType)
                {
                    // Unexpected exception
                    Utils.Trace(
                        Utils.TraceMasks.Information,
                        "Current configuration does not contain PubSubConnectionDataType with ConfigId = {0}",
                        connectionId);
                    return StatusCodes.BadNodeIdUnknown;
                }
                return RemoveConnection(pubSubConnectionDataType);
            }
        }

        /// <summary>
        /// Removes a connection from current configuration.
        /// </summary>
        /// <param name="pubSubConnectionDataType">The connection to be removed.</param>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The Connection was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the Connection.
        /// </returns>
        public StatusCode RemoveConnection(PubSubConnectionDataType pubSubConnectionDataType)
        {
            try
            {
                lock (m_lock)
                {
                    uint connectionId = FindIdForObject(pubSubConnectionDataType);
                    if (pubSubConnectionDataType != null && connectionId != InvalidId)
                    {
                        // remove children
                        foreach (
                            WriterGroupDataType writerGroup in new WriterGroupDataTypeCollection(
                                pubSubConnectionDataType.WriterGroups))
                        {
                            RemoveWriterGroup(writerGroup);
                        }
                        foreach (
                            ReaderGroupDataType readerGroup in new ReaderGroupDataTypeCollection(
                                pubSubConnectionDataType.ReaderGroups))
                        {
                            RemoveReaderGroup(readerGroup);
                        }
                        PubSubConfiguration.Connections.Remove(pubSubConnectionDataType);

                        //remove all references from dictionaries
                        m_idsToObjects.Remove(connectionId);
                        m_objectsToIds.Remove(pubSubConnectionDataType);
                        m_idsToParentId.Remove(connectionId);
                        m_idsToPubSubState.Remove(connectionId);

                        ConnectionRemoved?.Invoke(
                            this,
                            new ConnectionEventArgs
                            {
                                ConnectionId = connectionId,
                                PubSubConnectionDataType = pubSubConnectionDataType
                            });
                        return StatusCodes.Good;
                    }
                    return StatusCodes.BadNodeIdUnknown;
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.RemoveConnection: Exception");
            }

            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Adds a writerGroup to the specified connection
        /// </summary>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The WriterGroup was added with success.
        /// - <see cref="StatusCodes.BadBrowseNameDuplicated"/> An Object with the name already exists.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error adding the WriterGroup.
        /// </returns>
        /// <exception cref="ArgumentException"></exception>
        public StatusCode AddWriterGroup(
            uint parentConnectionId,
            WriterGroupDataType writerGroupDataType)
        {
            if (m_objectsToIds.ContainsKey(writerGroupDataType))
            {
                throw new ArgumentException(
                    "This WriterGroupDataType instance is already added to the configuration.");
            }
            if (!m_idsToObjects.TryGetValue(parentConnectionId, out object value))
            {
                throw new ArgumentException(
                    Utils.Format(
                        "There is no connection with configurationId = {0} in current configuration.",
                        parentConnectionId));
            }
            try
            {
                lock (m_lock)
                {
                    // remember collections
                    var dataSetWriters = new DataSetWriterDataTypeCollection(
                        writerGroupDataType.DataSetWriters);
                    writerGroupDataType.DataSetWriters.Clear();
                    if (value is PubSubConnectionDataType parentConnection)
                    {
                        //validate duplicate name
                        bool duplicateName = false;
                        foreach (WriterGroupDataType writerGroup in parentConnection.WriterGroups)
                        {
                            if (writerGroup.Name == writerGroupDataType.Name)
                            {
                                duplicateName = true;
                                break;
                            }
                        }
                        if (duplicateName)
                        {
                            Utils.Trace(
                                Utils.TraceMasks.Error,
                                "Attempted to add WriterGroupDataType with duplicate name = {0}",
                                writerGroupDataType.Name);
                            return StatusCodes.BadBrowseNameDuplicated;
                        }

                        uint newWriterGroupId = m_nextId++;
                        //remember writer group
                        m_idsToObjects.Add(newWriterGroupId, writerGroupDataType);
                        m_objectsToIds.Add(writerGroupDataType, newWriterGroupId);
                        parentConnection.WriterGroups.Add(writerGroupDataType);

                        // remember parent id
                        m_idsToParentId.Add(newWriterGroupId, parentConnectionId);
                        //remember initial state
                        m_idsToPubSubState.Add(
                            newWriterGroupId,
                            GetInitialPubSubState(writerGroupDataType));

                        // raise WriterGroupAdded event
                        WriterGroupAdded?.Invoke(
                            this,
                            new WriterGroupEventArgs
                            {
                                ConnectionId = parentConnectionId,
                                WriterGroupId = newWriterGroupId,
                                WriterGroupDataType = writerGroupDataType
                            });

                        //handler datasetWriters
                        foreach (DataSetWriterDataType datasetWriter in dataSetWriters)
                        {
                            // handle empty names
                            if (string.IsNullOrEmpty(datasetWriter.Name))
                            {
                                //set default name
                                datasetWriter.Name = "DataSetWriter_" + (m_nextId + 1);
                            }
                            AddDataSetWriter(newWriterGroupId, datasetWriter);
                        }

                        return StatusCodes.Good;
                    }
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.AddWriterGroup: Exception");
            }
            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Removes a WriterGroupDataType instance from current configuration specified by configId
        /// </summary>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The WriterGroup was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the WriterGroup.
        /// </returns>
        public StatusCode RemoveWriterGroup(uint writerGroupId)
        {
            lock (m_lock)
            {
                if (FindObjectById(writerGroupId) is not WriterGroupDataType writerGroupDataType)
                {
                    // Unexpected exception
                    Utils.Trace(
                        Utils.TraceMasks.Information,
                        "Current configuration does not contain WriterGroupDataType with ConfigId = {0}",
                        writerGroupId);
                    return StatusCodes.BadNodeIdUnknown;
                }
                return RemoveWriterGroup(writerGroupDataType);
            }
        }

        /// <summary>
        /// Removes a WriterGroupDataType instance from current configuration
        /// </summary>
        /// <param name="writerGroupDataType">Instance to remove</param>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The WriterGroup was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the WriterGroup.
        /// </returns>
        public StatusCode RemoveWriterGroup(WriterGroupDataType writerGroupDataType)
        {
            try
            {
                lock (m_lock)
                {
                    uint writerGroupId = FindIdForObject(writerGroupDataType);
                    if (writerGroupDataType != null && writerGroupId != InvalidId)
                    {
                        // remove children
                        foreach (
                            DataSetWriterDataType dataSetWriter in new DataSetWriterDataTypeCollection(
                                writerGroupDataType.DataSetWriters))
                        {
                            RemoveDataSetWriter(dataSetWriter);
                        }
                        // find parent connection
                        var parentConnection = FindParentForObject(
                            writerGroupDataType) as PubSubConnectionDataType;
                        uint parentConnectionId = FindIdForObject(parentConnection);
                        if (parentConnection != null && parentConnectionId != InvalidId)
                        {
                            parentConnection.WriterGroups.Remove(writerGroupDataType);

                            //remove all references from dictionaries
                            m_idsToObjects.Remove(writerGroupId);
                            m_objectsToIds.Remove(writerGroupDataType);
                            m_idsToParentId.Remove(writerGroupId);
                            m_idsToPubSubState.Remove(writerGroupId);

                            WriterGroupRemoved?.Invoke(
                                this,
                                new WriterGroupEventArgs
                                {
                                    WriterGroupId = writerGroupId,
                                    WriterGroupDataType = writerGroupDataType,
                                    ConnectionId = parentConnectionId
                                });
                            return StatusCodes.Good;
                        }
                    }

                    return StatusCodes.BadNodeIdUnknown;
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.RemoveWriterGroup: Exception");
            }

            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Adds a DataSetWriter to the specified writer group
        /// </summary>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The DataSetWriter was added with success.
        /// - <see cref="StatusCodes.BadBrowseNameDuplicated"/> An Object with the name already exists.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error adding the DataSetWriter.
        /// </returns>
        /// <exception cref="ArgumentException"></exception>
        public StatusCode AddDataSetWriter(
            uint parentWriterGroupId,
            DataSetWriterDataType dataSetWriterDataType)
        {
            if (m_objectsToIds.ContainsKey(dataSetWriterDataType))
            {
                throw new ArgumentException(
                    "This DataSetWriterDataType instance is already added to the configuration.");
            }
            if (!m_idsToObjects.TryGetValue(parentWriterGroupId, out object value))
            {
                throw new ArgumentException(
                    Utils.Format(
                        "There is no WriterGroup with configurationId = {0} in current configuration.",
                        parentWriterGroupId));
            }
            try
            {
                lock (m_lock)
                {
                    if (value is WriterGroupDataType parentWriterGroup)
                    {
                        //validate duplicate name
                        bool duplicateName = false;
                        foreach (DataSetWriterDataType writer in parentWriterGroup.DataSetWriters)
                        {
                            if (writer.Name == dataSetWriterDataType.Name)
                            {
                                duplicateName = true;
                                break;
                            }
                        }
                        if (duplicateName)
                        {
                            Utils.Trace(
                                Utils.TraceMasks.Error,
                                "Attempted to add DataSetWriterDataType with duplicate name = {0}",
                                dataSetWriterDataType.Name);
                            return StatusCodes.BadBrowseNameDuplicated;
                        }

                        uint newDataSetWriterId = m_nextId++;
                        //remember connection
                        m_idsToObjects.Add(newDataSetWriterId, dataSetWriterDataType);
                        m_objectsToIds.Add(dataSetWriterDataType, newDataSetWriterId);
                        parentWriterGroup.DataSetWriters.Add(dataSetWriterDataType);

                        // remember parent id
                        m_idsToParentId.Add(newDataSetWriterId, parentWriterGroupId);

                        //remember initial state
                        m_idsToPubSubState.Add(
                            newDataSetWriterId,
                            GetInitialPubSubState(dataSetWriterDataType));

                        // raise DataSetWriterAdded event
                        DataSetWriterAdded?.Invoke(
                            this,
                            new DataSetWriterEventArgs
                            {
                                WriterGroupId = parentWriterGroupId,
                                DataSetWriterId = newDataSetWriterId,
                                DataSetWriterDataType = dataSetWriterDataType
                            });

                        return StatusCodes.Good;
                    }
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.AddDataSetWriter: Exception");
            }
            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Removes a DataSetWriterDataType instance from current configuration specified by configId
        /// </summary>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The DataSetWriter was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the DataSetWriter.
        /// </returns>
        public StatusCode RemoveDataSetWriter(uint dataSetWriterId)
        {
            lock (m_lock)
            {
                if (FindObjectById(
                    dataSetWriterId) is not DataSetWriterDataType dataSetWriterDataType)
                {
                    // Unexpected exception
                    Utils.Trace(
                        Utils.TraceMasks.Information,
                        "Current configuration does not contain DataSetWriterDataType with ConfigId = {0}",
                        dataSetWriterId);
                    return StatusCodes.BadNodeIdUnknown;
                }
                return RemoveDataSetWriter(dataSetWriterDataType);
            }
        }

        /// <summary>
        /// Removes a DataSetWriterDataType instance from current configuration
        /// </summary>
        /// <param name="dataSetWriterDataType">Instance to remove</param>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The DataSetWriter was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the DataSetWriter.
        /// </returns>
        public StatusCode RemoveDataSetWriter(DataSetWriterDataType dataSetWriterDataType)
        {
            try
            {
                lock (m_lock)
                {
                    uint dataSetWriterId = FindIdForObject(dataSetWriterDataType);
                    if (dataSetWriterDataType != null && dataSetWriterId != InvalidId)
                    {
                        // find parent writerGroup
                        var parentWriterGroup = FindParentForObject(
                            dataSetWriterDataType) as WriterGroupDataType;
                        uint parentWriterGroupId = FindIdForObject(parentWriterGroup);
                        if (parentWriterGroup != null && parentWriterGroupId != InvalidId)
                        {
                            parentWriterGroup.DataSetWriters.Remove(dataSetWriterDataType);

                            //remove all references from dictionaries
                            m_idsToObjects.Remove(dataSetWriterId);
                            m_objectsToIds.Remove(dataSetWriterDataType);
                            m_idsToParentId.Remove(dataSetWriterId);
                            m_idsToPubSubState.Remove(dataSetWriterId);

                            DataSetWriterRemoved?.Invoke(
                                this,
                                new DataSetWriterEventArgs
                                {
                                    WriterGroupId = parentWriterGroupId,
                                    DataSetWriterDataType = dataSetWriterDataType,
                                    DataSetWriterId = dataSetWriterId
                                });
                            return StatusCodes.Good;
                        }
                    }
                    return StatusCodes.BadNodeIdUnknown;
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.RemoveDataSetWriter: Exception");
            }

            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Adds a readerGroup to the specified connection
        /// </summary>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The ReaderGroup was added with success.
        /// - <see cref="StatusCodes.BadBrowseNameDuplicated"/> An Object with the name already exists.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error adding the ReaderGroup.
        /// </returns>
        /// <exception cref="ArgumentException"></exception>
        public StatusCode AddReaderGroup(
            uint parentConnectionId,
            ReaderGroupDataType readerGroupDataType)
        {
            if (m_objectsToIds.ContainsKey(readerGroupDataType))
            {
                throw new ArgumentException(
                    "This ReaderGroupDataType instance is already added to the configuration.");
            }
            if (!m_idsToObjects.TryGetValue(parentConnectionId, out object value))
            {
                throw new ArgumentException(
                    Utils.Format(
                        "There is no connection with configurationId = {0} in current configuration.",
                        parentConnectionId));
            }
            try
            {
                lock (m_lock)
                {
                    // remember collections
                    var dataSetReaders = new DataSetReaderDataTypeCollection(
                        readerGroupDataType.DataSetReaders);
                    readerGroupDataType.DataSetReaders.Clear();
                    if (value is PubSubConnectionDataType parentConnection)
                    {
                        //validate duplicate name
                        bool duplicateName = false;
                        foreach (ReaderGroupDataType readerGroup in parentConnection.ReaderGroups)
                        {
                            if (readerGroup.Name == readerGroupDataType.Name)
                            {
                                duplicateName = true;
                                break;
                            }
                        }
                        if (duplicateName)
                        {
                            Utils.Trace(
                                Utils.TraceMasks.Error,
                                "Attempted to add ReaderGroupDataType with duplicate name = {0}",
                                readerGroupDataType.Name);
                            return StatusCodes.BadBrowseNameDuplicated;
                        }

                        uint newReaderGroupId = m_nextId++;
                        //remember reader group
                        m_idsToObjects.Add(newReaderGroupId, readerGroupDataType);
                        m_objectsToIds.Add(readerGroupDataType, newReaderGroupId);
                        parentConnection.ReaderGroups.Add(readerGroupDataType);

                        // remember parent id
                        m_idsToParentId.Add(newReaderGroupId, parentConnectionId);

                        //remember initial state
                        m_idsToPubSubState.Add(
                            newReaderGroupId,
                            GetInitialPubSubState(readerGroupDataType));

                        // raise ReaderGroupAdded event
                        ReaderGroupAdded?.Invoke(
                            this,
                            new ReaderGroupEventArgs
                            {
                                ConnectionId = parentConnectionId,
                                ReaderGroupId = newReaderGroupId,
                                ReaderGroupDataType = readerGroupDataType
                            });

                        //handler datasetWriters
                        foreach (DataSetReaderDataType datasetReader in dataSetReaders)
                        {
                            // handle empty names
                            if (string.IsNullOrEmpty(datasetReader.Name))
                            {
                                //set default name
                                datasetReader.Name = "DataSetReader_" + (m_nextId + 1);
                            }
                            AddDataSetReader(newReaderGroupId, datasetReader);
                        }

                        return StatusCodes.Good;
                    }
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.AddReaderGroup: Exception");
            }
            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Removes a ReaderGroupDataType instance from current configuration specified by configId
        /// </summary>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The ReaderGroup was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the ReaderGroup.
        /// </returns>
        public StatusCode RemoveReaderGroup(uint readerGroupId)
        {
            lock (m_lock)
            {
                if (FindObjectById(readerGroupId) is not ReaderGroupDataType readerGroupDataType)
                {
                    Utils.Trace(
                        Utils.TraceMasks.Information,
                        "Current configuration does not contain ReaderGroupDataType with ConfigId = {0}",
                        readerGroupId);
                    return StatusCodes.BadInvalidArgument;
                }
                return RemoveReaderGroup(readerGroupDataType);
            }
        }

        /// <summary>
        /// Removes a ReaderGroupDataType instance from current configuration
        /// </summary>
        /// <param name="readerGroupDataType">Instance to remove</param>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The ReaderGroup was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the ReaderGroup.
        /// </returns>
        public StatusCode RemoveReaderGroup(ReaderGroupDataType readerGroupDataType)
        {
            try
            {
                lock (m_lock)
                {
                    uint readerGroupId = FindIdForObject(readerGroupDataType);
                    if (readerGroupDataType != null && readerGroupId != InvalidId)
                    {
                        // remove children
                        foreach (
                            DataSetReaderDataType dataSetReader in new DataSetReaderDataTypeCollection(
                                readerGroupDataType.DataSetReaders))
                        {
                            RemoveDataSetReader(dataSetReader);
                        }
                        // find parent connection
                        var parentConnection = FindParentForObject(
                            readerGroupDataType) as PubSubConnectionDataType;
                        uint parentConnectionId = FindIdForObject(parentConnection);
                        if (parentConnection != null && parentConnectionId != InvalidId)
                        {
                            parentConnection.ReaderGroups.Remove(readerGroupDataType);

                            //remove all references from dictionaries
                            m_idsToObjects.Remove(readerGroupId);
                            m_objectsToIds.Remove(readerGroupDataType);
                            m_idsToParentId.Remove(readerGroupId);
                            m_idsToPubSubState.Remove(readerGroupId);

                            ReaderGroupRemoved?.Invoke(
                                this,
                                new ReaderGroupEventArgs
                                {
                                    ReaderGroupId = readerGroupId,
                                    ReaderGroupDataType = readerGroupDataType,
                                    ConnectionId = parentConnectionId
                                });
                            return StatusCodes.Good;
                        }
                    }

                    return StatusCodes.BadNodeIdUnknown;
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.RemoveReaderGroup: Exception");
            }

            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Adds a DataSetReader to the specified reader group
        /// </summary>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The DataSetReader was added with success.
        /// - <see cref="StatusCodes.BadBrowseNameDuplicated"/> An Object with the name already exists.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error adding the DataSetReader.
        /// </returns>
        /// <exception cref="ArgumentException"></exception>
        public StatusCode AddDataSetReader(
            uint parentReaderGroupId,
            DataSetReaderDataType dataSetReaderDataType)
        {
            if (m_objectsToIds.ContainsKey(dataSetReaderDataType))
            {
                throw new ArgumentException(
                    "This DataSetReaderDataType instance is already added to the configuration.");
            }
            if (!m_idsToObjects.TryGetValue(parentReaderGroupId, out object value))
            {
                throw new ArgumentException(
                    Utils.Format(
                        "There is no ReaderGroup with configurationId = {0} in current configuration.",
                        parentReaderGroupId));
            }
            try
            {
                lock (m_lock)
                {
                    if (value is ReaderGroupDataType parentReaderGroup)
                    {
                        //validate duplicate name
                        bool duplicateName = false;
                        foreach (DataSetReaderDataType reader in parentReaderGroup.DataSetReaders)
                        {
                            if (reader.Name == dataSetReaderDataType.Name)
                            {
                                duplicateName = true;
                                break;
                            }
                        }
                        if (duplicateName)
                        {
                            Utils.Trace(
                                Utils.TraceMasks.Error,
                                "Attempted to add DataSetReaderDataType with duplicate name = {0}",
                                dataSetReaderDataType.Name);
                            return StatusCodes.BadBrowseNameDuplicated;
                        }

                        uint newDataSetReaderId = m_nextId++;
                        //remember connection
                        m_idsToObjects.Add(newDataSetReaderId, dataSetReaderDataType);
                        m_objectsToIds.Add(dataSetReaderDataType, newDataSetReaderId);
                        parentReaderGroup.DataSetReaders.Add(dataSetReaderDataType);

                        // remember parent id
                        m_idsToParentId.Add(newDataSetReaderId, parentReaderGroupId);

                        //remember initial state
                        m_idsToPubSubState.Add(
                            newDataSetReaderId,
                            GetInitialPubSubState(dataSetReaderDataType));

                        // raise WriterGroupAdded event
                        DataSetReaderAdded?.Invoke(
                            this,
                            new DataSetReaderEventArgs
                            {
                                ReaderGroupId = parentReaderGroupId,
                                DataSetReaderId = newDataSetReaderId,
                                DataSetReaderDataType = dataSetReaderDataType
                            });

                        return StatusCodes.Good;
                    }
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.AddDataSetReader: Exception");
            }
            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Removes a DataSetReaderDataType instance from current configuration specified by configId
        /// </summary>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The DataSetWriter was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the DataSetWriter.
        /// </returns>
        public StatusCode RemoveDataSetReader(uint dataSetReaderId)
        {
            lock (m_lock)
            {
                if (FindObjectById(
                    dataSetReaderId) is not DataSetReaderDataType dataSetReaderDataType)
                {
                    // Unexpected exception
                    Utils.Trace(
                        Utils.TraceMasks.Information,
                        "Current configuration does not contain DataSetReaderDataType with ConfigId = {0}",
                        dataSetReaderId);
                    return StatusCodes.BadNodeIdUnknown;
                }
                return RemoveDataSetReader(dataSetReaderDataType);
            }
        }

        /// <summary>
        /// Removes a DataSetReaderDataType instance from current configuration
        /// </summary>
        /// <param name="dataSetReaderDataType">Instance to remove</param>
        /// <returns>
        /// - <see cref="StatusCodes.Good"/> The DataSetWriter was removed with success.
        /// - <see cref="StatusCodes.BadNodeIdUnknown"/> The GroupId is unknown.
        /// - <see cref="StatusCodes.BadInvalidArgument"/> There was an error removing the DataSetWriter.
        /// </returns>
        public StatusCode RemoveDataSetReader(DataSetReaderDataType dataSetReaderDataType)
        {
            try
            {
                lock (m_lock)
                {
                    uint dataSetReaderId = FindIdForObject(dataSetReaderDataType);
                    if (dataSetReaderDataType != null && dataSetReaderId != InvalidId)
                    {
                        // find parent readerGroup
                        var parentWriterGroup = FindParentForObject(
                            dataSetReaderDataType) as ReaderGroupDataType;
                        uint parenReaderGroupId = FindIdForObject(parentWriterGroup);
                        if (parentWriterGroup != null && parenReaderGroupId != InvalidId)
                        {
                            parentWriterGroup.DataSetReaders.Remove(dataSetReaderDataType);

                            //remove all references from dictionaries
                            m_idsToObjects.Remove(dataSetReaderId);
                            m_objectsToIds.Remove(dataSetReaderDataType);
                            m_idsToParentId.Remove(dataSetReaderId);
                            m_idsToPubSubState.Remove(dataSetReaderId);

                            DataSetReaderRemoved?.Invoke(
                                this,
                                new DataSetReaderEventArgs
                                {
                                    ReaderGroupId = parenReaderGroupId,
                                    DataSetReaderDataType = dataSetReaderDataType,
                                    DataSetReaderId = dataSetReaderId
                                });
                            return StatusCodes.Good;
                        }
                    }
                    return StatusCodes.BadNodeIdUnknown;
                }
            }
            catch (Exception ex)
            {
                // Unexpected exception
                Utils.Trace(ex, "UaPubSubConfigurator.RemoveDataSetReader: Exception");
            }

            return StatusCodes.BadInvalidArgument;
        }

        /// <summary>
        /// Enable the specified configuration object specified by Id
        /// </summary>
        public StatusCode Enable(uint configurationId)
        {
            return Enable(FindObjectById(configurationId));
        }

        /// <summary>
        /// Enable the specified configuration object
        /// </summary>
        /// <exception cref="ArgumentException"><paramref name="configurationObject"/></exception>
        public StatusCode Enable(object configurationObject)
        {
            if (configurationObject == null)
            {
                throw new ArgumentException(
                    "The parameter cannot be null.",
                    nameof(configurationObject));
            }
            if (!m_objectsToIds.ContainsKey(configurationObject))
            {
                throw new ArgumentException(
                    "This {0} instance is not part of current configuration.",
                    configurationObject.GetType().Name);
            }
            PubSubState currentState = FindStateForObject(configurationObject);
            if (currentState != PubSubState.Disabled)
            {
                Utils.Trace(
                    Utils.TraceMasks.Information,
                    "Attempted to call Enable() on an object that is not in Disabled state");
                return StatusCodes.BadInvalidState;
            }
            PubSubState parentState = PubSubState.Operational;
            if (configurationObject != PubSubConfiguration)
            {
                parentState = FindStateForObject(FindParentForObject(configurationObject));
            }

            if (parentState == PubSubState.Operational)
            {
                // Enabled and parent Operational
                SetStateForObject(configurationObject, PubSubState.Operational);
            }
            else
            {
                // Enabled but parent not Operational
                SetStateForObject(configurationObject, PubSubState.Paused);
            }
            UpdateChildrenState(configurationObject);
            return StatusCodes.Good;
        }

        /// <summary>
        /// Disable the specified configuration object specified by Id
        /// </summary>
        public StatusCode Disable(uint configurationId)
        {
            return Disable(FindObjectById(configurationId));
        }

        /// <summary>
        /// Disable the specified configuration object
        /// </summary>
        /// <exception cref="ArgumentException"><paramref name="configurationObject"/></exception>
        public StatusCode Disable(object configurationObject)
        {
            if (configurationObject == null)
            {
                throw new ArgumentException(
                    "The parameter cannot be null.",
                    nameof(configurationObject));
            }
            if (!m_objectsToIds.ContainsKey(configurationObject))
            {
                throw new ArgumentException(
                    "This {0} instance is not part of current configuration.",
                    configurationObject.GetType().Name);
            }
            PubSubState currentState = FindStateForObject(configurationObject);
            if (currentState == PubSubState.Disabled)
            {
                Utils.Trace(
                    Utils.TraceMasks.Information,
                    "Attempted to call Disable() on an object that is already in Disabled state");
                return StatusCodes.BadInvalidState;
            }

            SetStateForObject(configurationObject, PubSubState.Disabled);

            UpdateChildrenState(configurationObject);
            return StatusCodes.Good;
        }

        /// <summary>
        /// Change state for the specified configuration object
        /// </summary>
        private void SetStateForObject(object configurationObject, PubSubState newState)
        {
            uint id = FindIdForObject(configurationObject);
            if (id != InvalidId && m_idsToPubSubState.TryGetValue(id, out PubSubState oldState))
            {
                m_idsToPubSubState[id] = newState;
                PubSubStateChanged?.Invoke(
                    this,
                    new PubSubStateChangedEventArgs
                    {
                        ConfigurationObject = configurationObject,
                        ConfigurationObjectId = id,
                        NewState = newState,
                        OldState = oldState
                    });
                bool configurationObjectEnabled
                    = newState is PubSubState.Operational or PubSubState.Paused;
                //update the Enabled flag in config object
                switch (configurationObject)
                {
                    case PubSubConfigurationDataType:
                        ((PubSubConfigurationDataType)configurationObject).Enabled
                            = configurationObjectEnabled;
                        break;
                    case PubSubConnectionDataType:
                        ((PubSubConnectionDataType)configurationObject).Enabled
                            = configurationObjectEnabled;
                        break;
                    case WriterGroupDataType:
                        ((WriterGroupDataType)configurationObject).Enabled
                            = configurationObjectEnabled;
                        break;
                    case DataSetWriterDataType:
                        ((DataSetWriterDataType)configurationObject).Enabled
                            = configurationObjectEnabled;
                        break;
                    case ReaderGroupDataType:
                        ((ReaderGroupDataType)configurationObject).Enabled
                            = configurationObjectEnabled;
                        break;
                    case DataSetReaderDataType:
                        ((DataSetReaderDataType)configurationObject).Enabled
                            = configurationObjectEnabled;
                        break;
                }
            }
        }

        /// <summary>
        /// Calculate and update the state for child objects of a configuration object (StATE MACHINE)
        /// </summary>
        private void UpdateChildrenState(object configurationObject)
        {
            PubSubState parentState = FindStateForObject(configurationObject);
            //find child ids
            List<uint> childrenIds = FindChildrenIdsForObject(configurationObject);
            if (parentState == PubSubState.Operational)
            {
                // Enabled and parent Operational
                foreach (uint childId in childrenIds)
                {
                    PubSubState childState = FindStateForId(childId);
                    if (childState == PubSubState.Paused)
                    {
                        // become Operational if Parent changed to Operational
                        object childObject = FindObjectById(childId);
                        SetStateForObject(childObject, PubSubState.Operational);

                        UpdateChildrenState(childObject);
                    }
                }
            }
            else if (parentState is PubSubState.Disabled or PubSubState.Paused)
            {
                // Parent changed to Disabled or Paused
                foreach (uint childId in childrenIds)
                {
                    PubSubState childState = FindStateForId(childId);
                    if (childState is PubSubState.Operational or PubSubState.Error)
                    {
                        // become Operational if Parent changed to Operational
                        object childObject = FindObjectById(childId);
                        SetStateForObject(childObject, PubSubState.Paused);

                        UpdateChildrenState(childObject);
                    }
                }
            }
        }

        /// <summary>
        /// Get <see cref="PubSubState"/> for an item depending on enabled flag and parent's <see cref="PubSubState"/>.
        /// </summary>
        /// <param name="enabled">Configured Enabled flag. </param>
        /// <param name="parentPubSubState"><see cref="PubSubState"/> of the parent configured object.</param>
        private static PubSubState GetInitialPubSubState(
            bool enabled,
            PubSubState parentPubSubState)
        {
            if (enabled)
            {
                if (parentPubSubState == PubSubState.Operational)
                {
                    // The PubSub component is operational.
                    return PubSubState.Operational;
                }
                // The PubSub component is enabled but currently paused by a parent component. The
                // parent component is either Disabled_0 or Paused_1.
                return PubSubState.Paused;
            }
            // PubSub component is configured but currently disabled.
            return PubSubState.Disabled;
        }

        /// <summary>
        /// Calculate and return the initial state of a pub sub data type configuration object
        /// </summary>
        private PubSubState GetInitialPubSubState(object configurationObject)
        {
            PubSubState parentPubSubState = PubSubState.Operational;

            bool configurationObjectEnabled;
            switch (configurationObject)
            {
                case PubSubConfigurationDataType:
                    configurationObjectEnabled = ((PubSubConfigurationDataType)configurationObject)
                        .Enabled;
                    break;
                case PubSubConnectionDataType:
                    configurationObjectEnabled = ((PubSubConnectionDataType)configurationObject)
                        .Enabled;
                    //find parent state
                    parentPubSubState = FindStateForObject(PubSubConfiguration);
                    break;
                case WriterGroupDataType:
                {
                    configurationObjectEnabled = ((WriterGroupDataType)configurationObject).Enabled;
                    //find parent connection
                    object parentConnection = FindParentForObject(configurationObject);
                    //find parent state
                    parentPubSubState = FindStateForObject(parentConnection);
                    break;
                }
                case DataSetWriterDataType:
                    configurationObjectEnabled = ((DataSetWriterDataType)configurationObject)
                        .Enabled;
                    //find parent
                    object parentWriterGroup = FindParentForObject(configurationObject);
                    //find parent state
                    parentPubSubState = FindStateForObject(parentWriterGroup);
                    break;
                case ReaderGroupDataType:
                {
                    configurationObjectEnabled = ((ReaderGroupDataType)configurationObject).Enabled;
                    //find parent connection
                    object parentConnection = FindParentForObject(configurationObject);
                    //find parent state
                    parentPubSubState = FindStateForObject(parentConnection);
                    break;
                }
                case DataSetReaderDataType:
                    configurationObjectEnabled = ((DataSetReaderDataType)configurationObject)
                        .Enabled;
                    //find parent
                    object parentReaderGroup = FindParentForObject(configurationObject);
                    //find parent state
                    parentPubSubState = FindStateForObject(parentReaderGroup);
                    break;
                default:
                    return PubSubState.Error;
            }
            return GetInitialPubSubState(configurationObjectEnabled, parentPubSubState);
        }
    }
}
